/*******************************************************************
 * (c) Arphen Lin, arphenlin@gmail.com
 *
 * Required:
 * 	prototype.js
 * 	jQuery.js
 * 	
 * References:
 *  http://wiki.greasespot.net/Code_Snippets
 *	http://userscripts.org/forums/1/topics/48
 *
 ********************************************************************/
// ==UserScript==
// @name          GM2Lib
// @description   Greasemonkey Library 
// @version				2007-08-15
// @namespace
// @include       *
// @exclude
// ==/UserScript==

/*******************************************************************
 * String extension
 *******************************************************************/
String.prototype.trim = function(){
	return this.replace(/(^\s*)|(\s*$)/g, "");
};

String.prototype.triml = function(){
	return this.replace(/(^\s*)/g, "");
};

String.prototype.trimr = function(){
	return this.replace(/(\s*$)/g, "");
}; 

/*******************************************************************
 * Array extension
 * http://4umi.com/web/javascript/array.htm
 *******************************************************************/

// boolean = Array.once() - try until the callback function return true, or to the end of array
// callback: f(v, i, a){ return (true|false); }
Array.prototype.once = function( f ) {	
	for(var i=0; i<this.length; i++){
		if( f(this[i], i, this) ) return true; // done once
	}
	return false; // never done
};

// void Array.each(f) - invoke the callback function with each array elements
// callback: f(v, i, a){ return (true|false); }
Array.prototype.each = function( f ) {
	for(var i=0; i<this.length; i++){
		f(this[i], i, this);
	}
};

// Array.insert( index, value ) - Insert value at index of this array
Array.prototype.insert = function( i, v ) {
	if( i>=0 ) {		
		// eg. this = [0,1,2,3]
		var a = this.splice(i);	// eg. i=2, this = [0,1], a = [2,3]
		this[i++] = v;					// this = [0,1,v]
		for(var j=0; j<a.length; j++){
			this[i++] = a[j];
		}
	}
};

// i = Array.indexOf( value, begin, strict ) - Return index of the first element that matches value
Array.prototype.indexOf = function( v, b, s ) {
	b = +b || 0;
	for(var i=b; i<this.length; i++){
		if( this[i]===v || s && this[i]==v ) { return i; }
	}
	return -1;	
};

// i = Array.lastIndexOf( value, begin, strict ) - Return index of the last element that matches value
Array.prototype.lastIndexOf = function( v, b, s ) {
	b = +b || 0;
	var i = this.length; 
	while(i-->b) {
		if( this[i]===v || s && this[i]==v ) { return i; }
	}
	return -1;
};

// [i] = Array.random( range ) - Return a random element, optionally up to or from range
Array.prototype.random = function( r ) {
	var i = 0, l = this.length;
	if( !r ) { r = this.length; }
	else if( r > 0 ) { r = r % l; }
	else { i = r; r = l + r % l; }
	return this[ Math.floor( r * Math.random() - i ) ];
};

// Array.shuffle( deep ) - Randomly interchange elements of this array
Array.prototype.shuffle = function( b ) {
	var i = this.length, j, t;
	while( i ) {
		j = Math.floor( ( i-- ) * Math.random() );
		t = b && typeof this[i].shuffle!=='undefined' ? this[i].shuffle() : this[i];
		this[i] = this[j];
		this[j] = t;
	}
};

// new[] = Array.unique( strict ) - Remove duplicate values and return the new array
Array.prototype.unique = function( b ) {
	var a = [], i, l = this.length;
	for( i=0; i<l; i++ ) {
		if( a.indexOf( this[i], 0, b ) < 0 ) { a.push( this[i] ); }
	}
	return a;
};

// new[] = Array.walk() - Change each value according to a callback function, and return the new array
Array.prototype.walk = function( f ) {
	var a = [], i = this.length;
	while(i--) { a.push( f( this[i] ) ); }
	return a.reverse();
};


/*******************************************************************
 * gm class
 *******************************************************************/
var gm = function(){};
// Extend
gm.extend = function(target, source){
	source = source || target || {};
	target = source == target ? this : target;
	
	for(var i in source)
		target[i] = source[i];
	
	return target;
};

// consts
gm.Consts = {
	// ajax hint
	OBJ_ID_AJAX_HINT: 'aplAjaxHint',
	
	CLASS_MOVABLE : 'CLASS_MOVABLE',
	CLASS_MOVING : 'CLASS_MOVING',
	// console
	OBJ_ID_CONSOLE_WRAPPER : 'OBJ_ID_CONSOLE_WRAPPER',
	OBJ_ID_CONSOLE_TOOLBAR : 'OBJ_ID_CONSOLE_TOOLBAR',
	OBJ_ID_CONSOLE : 'OBJ_ID_CONSOLE',
	
	OBJ_ID_HIDDEN_DIV: 'OBJ_ID_HIDDEN_DIV',
	MAX_DUMP_DEPTH: 10,
	ENUM_ONOFF: {
		OFF: 0,
		ON: 1
	},
	
	ENUM_GM_LOG: {
		INFO: 0,
		WARN: 1,
		ERR: 2
	}
};

// Essential functions
gm.extend({
	
});

// Utilities
gm.Utils = {
	hash: function(len){
		if(!len) len=20;
		return Math.random().toString().substring(2, len);
	},
	
	dump: function (obj, name, indent, depth) {
		if (depth > gm.Consts.MAX_DUMP_DEPTH) {
			return indent + name + ": <Maximum Depth Reached>\n";
		}
		if (typeof obj == "object") {
			var child = null;
			var output = indent + name + "\n";
			indent += "\t";
			for (var item in obj)
			{
				try {
					child = obj[item];
				} catch (e) {
					child = "<Unable to Evaluate>";
				}
				if (typeof child == "object") {
					output += _dumpObj(child, item, indent, depth + 1);
				} else {
					output += indent + item + ": " + child + "\n";
				}
			}
			return output;
		} else {
			return obj;
		}
	},
	
	getUrlParams: function(url){
		try{
			url = decodeURIComponent(url);
		}catch(e){
			//gm.Console.log(url);
		}
		var s='';
		var i=url.indexOf('?');
		if(i>=0)	s = url.substring(i+1, url.length);
	
		var urlParams = {
			//'key1': 'value1',
			//'key2': 'value2',
			//...
			//'keyN': 'valueN'
		};
	
		// parameters
		if(s.length>0){
			var pairs = s.split('&');
			pairs.each(function(p){
				var ps = p.split('=');
				if(ps.length==2){
					urlParams[ps[0]] = ps[1];
				}
			});
		}
	
		return urlParams;
	},
	
	cssToString: function (css) {
		if(!css)	return '';
	
		var output = '';
		for (var item in css)
		{
			if(item=='length')	continue;
			if(isNaN(parseInt(item)))
				output += (output.length==0?'':'&') + item + "=" + css[item];
		}		
		return output;
	},
	
	dragDropper: {
		selObj: null,
		orgCursor: null,   // The original Cursor (mouse) Style so we can restore it
		dragOK: false,    // True if we're allowed to move the element under mouse
		dragXoffset: 0,    // How much we've moved the element on the horozontal
		dragYoffset: 0,    // How much we've moved the element on the verticle
	
		moveHandler: function(e){
			//gm.Console.log('moveHandler');
			if (e == null) { e = window.event }
			if (e.button<=1&&this.dragOK){
				this.selObj.style.left=e.clientX-this.dragXoffset+'px';
				this.selObj.style.top=e.clientY-this.dragYoffset+'px';
				return false;
			}
			return true;
		},
	
		cleanup: function(e) {
			this.selObj.removeEventListener('mousemove', gm.Utils.dragDropper.moveHandler, true);
			this.selObj.removeEventListener('mouseup', gm.Utils.dragDropper.cleanup, true);		
	
			//this.selObj.style.cursor=this.orgCursor;
			this.selObj.style.cursor='move';
			this.dragOK=false;
			this.removeClassName(gm.Consts.CLASS_MOVING);
		},
	
		dragHandler: function(e){		
			//gm.Console.log('dragHandler');
			var htype='-moz-grabbing';
			if (e == null) { e = window.event; htype='move';}
			var target = e.target != null ? e.target : e.srcElement;
			this.selObj=target;
			this.orgCursor=target.style.cursor;		
			
			//if (target.className.search(gm.Consts.CLASS_MOVABLE)>=0) {
			if(target.hasClassName(gm.Consts.CLASS_MOVABLE)){
				target.addClassName(gm.Consts.CLASS_MOVING);
				target.style.cursor=htype;
				this.dragOK=true;
				this.dragXoffset=e.clientX-parseInt(this.selObj.style.left);
				this.dragYoffset=e.clientY-parseInt(this.selObj.style.top);
				//document.onmousemove=gm.Utils.dragDropper.moveHandler;
				//document.onmouseup=gm.Utils.dragDropper.cleanup;
				//document.addEventListener('mousemove', gm.Utils.dragDropper.moveHandler, true);
				//document.addEventListener('mouseup', gm.Utils.dragDropper.cleanup, true);
				this.selObj.addEventListener('mousemove', gm.Utils.dragDropper.moveHandler, true);
				this.selObj.addEventListener('mouseup', gm.Utils.dragDropper.cleanup, true);
				return false;
			}
			return true;
		},
		
	
		close: function(){
			//document.removeEventListener('mousedown', gm.Utils.dragDropper.dragHandler, true);
		},
		
		add: function(obj){
			obj.addClassName(gm.Consts.CLASS_MOVABLE);
			obj.style.cursor = 'move';
			obj.addEventListener('mousedown', gm.Utils.dragDropper.dragHandler, true);
		},
		
		remove: function(obj){
			obj.removeClassName(gm.Consts.CLASS_MOVABLE);
			obj.removeEventListener('mousedown', gm.Utils.dragDropper.dragHandler, true);
		}
	}
	
};

// DOM
gm.Dom = {
	// create element by tag name, eg. _e('div');
	_e: function(tag) {
		return document.createElement(tag);
	},
	
	// create text node, eg. _t('hello, text');
	_t: function(text) {
		return document.createTextNode(text);
	},
	
	// get elements by tag name(s)
	$t: function() { // $t('div'); $t('h1,h2,h3'); $t(obj, 'a,span,textarea');
		var obj=null, list='';
		switch(arguments.length){
			case 1:
				obj = document;
				list = arguments[0];
				break;
			case 2:
				obj = arguments[0];			
				list = arguments[1];			
				break;
			default:
				return null;
		}
	
		var tagNames = list.split(',');
		var resultArray = new Array();
		for (var i=0;i<tagNames.length;i++)	{
			var tags = obj.getElementsByTagName(tagNames[i]);
			for (var j=0;j<tags.length;j++)
			{
				resultArray.push(tags[j]);
			}
		}
		var testNode = resultArray[0];
		if (testNode.sourceIndex)	{
			resultArray.sort(function (a,b) {
					return a.sourceIndex - b.sourceIndex;
			});
		}
		else if (testNode.compareDocumentPosition){
			resultArray.sort(function (a,b) {
					return 3 - (a.compareDocumentPosition(b) & 6);
			});
		}
		return resultArray;
	},
	
	// xpath to get object list
	$x: function (xpath, root){	// root: optional
		var doc = root ? (root.evaluate? root: root.ownerDocument): document;
		var snap = doc.evaluate( xpath, root||doc, null, null, null );
		var result = [];
		while(true){
			var nextItem = snap.iterateNext();
			if(nextItem)
				result.push(nextItem);
			else
				return result;
		}
	},
	
	// xpath to get first object
	$x0: function (xpath, root) {
		var objs = gm.Dom.$x(xpath, root);
		return (objs.length>0? objs[0]: null);
	},	
	
	// remove element
	del: function (e) {
		if(e.length == undefined){	// single object
			e.parentNode.removeChild(e);
		}else{	// array
			e.each(function(ei){
				ei.parentNode.removeChild(ei);
			});
		}
	},
	
	// append element
	add: function() {	// [parent], node
		switch(arguments.length){
			case 1:
				document.body.appendChild(arguments[0]); 
				break;
			case 2:
				arguments[0].appendChild(arguments[1]); 
				break;
		}	
	},

	// insert element before ...
	insert: function (newNode, node){	
		node.parentNode.insertBefore(newNode, node); 
	},
	
	// insert element after ...
	insertAfter: function (newNode, node){
		if(node.nextSibling)
			node.parentNode.insertBefore(newNode, node.nextSibling); 
		else
			gm.Dom.add(node.parentNode, newNode);
	},
	
	// exchange 2 elements
	exchange: function (obj1, obj2){
		var obj1Tmp = obj1.cloneNode(true);
		var obj2Tmp = obj2.cloneNode(true);
		obj1.parentNode.replaceChild(obj2Tmp, obj1);
		obj2.parentNode.replaceChild(obj1Tmp, obj2);	
	},
	
	// replace element with another
	replace: function (newObj, oldObj) {
		oldObj.parentNode.replaceChild(newObj, oldObj);
	},
	
	// insert CSS
	// Ex: _css('.date {font-size: 50pt !important;}');
	css: function(css){
		var head = _t('head')[0];
		if(!head) { return; }
		var style = gm.Dom._e('style');
		style.type = 'text/css';
		style.innerHTML = css;
		head.appendChild(style);
	}
	
	
};

// HTML
gm.Html = {
	submitForm: function (f){
		// simulate a button-click action 
		var b = gm.Dom._e('button');
		f.appendChild(b);
		b.click();
	},
	
	// load javascript dynamically =====================================================================
	loadJS: function (jsUrl){
		//<script src="/javascripts/ebnf.js" type="text/javascript" language="JavaScript"></script>
		var s = gm.Dom._e('script');
		s.setAttribute('type', 'text/javascript');
		s.setAttribute('language', 'JavaScript');
		s.setAttribute('src', jsUrl);	
		gm.Dom.add(gm.Dom.$x0("//head"), s);		
	},
	
	// insert javascript code 
	insertJS: function (js_code){
		var s = gm.Dom._e('script');
		s.setAttribute('type', 'text/javascript');
		s.innerHTML = js_code; // eg. function(x){...}
		gm.Dom.add(gm.Dom.$x0("//head"), s);	
	},
	
	loadCSS: function (cssUrl){
		//<link href="/stylesheets/main.css" rel="stylesheet" type="text/css" />
		var s = gm.Dom._e('link');
		s.setAttribute('type', 'text/css');
		s.setAttribute('rel', 'stylesheet');
		s.setAttribute('href', cssUrl);	
		gm.Dom.add(gm.Dom.$x0("//head"), s);	
	},
	
	setTitle: function (s){
		document.title = s;
	},
	
	addColor: function (text, color){
		return('<span style="font-weight: bold; color:'+color+'">'+text+'</span>');
	}
	
	
};
// Console
gm.Console = {
	id: 'gmConsole',

	_consoleWrapper: null,
	_console: null,
	_counter: 0,
	
	_formatText: function (msg){	
		return '<p class="row'+(gm.Console._counter++ % 2)+'">' + msg + '</p>';		
	},
	
	_addToolbarButton: function(options){
		var toolbar = $(gm.Consts.OBJ_ID_CONSOLE_TOOLBAR);
		options.each(function(op){
			var a = gm.Dom._e('a');		
			a.href = '#';
			a.innerHTML = op.text;
			a.addEventListener('click', op.handler, true);
			gm.Dom.add(toolbar, a);
		});
	},
	
	open : function(id){
		if(id) gm.Console.id = id;
		
		if(gm.Console._console) {
			gm.Console._consoleWrapper.style.display = 'block';
			return;
		}
		
		var div = gm.Dom._e('div');
		div.id = gm.Consts.OBJ_ID_CONSOLE_WRAPPER;
		div.setAttribute('style', 
			'position: absolute; z-index: 999; top: 100px; left: 100px;'
		);
		
		div.innerHTML = gm.Console.id +
		'<div id="'+gm.Consts.OBJ_ID_CONSOLE_TOOLBAR+'"></div>'+
		'<div id="'+gm.Consts.OBJ_ID_CONSOLE+'"></div>';
		gm.Dom.add(div);
		
		gm.Console._consoleWrapper = $(gm.Consts.OBJ_ID_CONSOLE_WRAPPER);
		gm.Console._console = $(gm.Consts.OBJ_ID_CONSOLE);
		
		gm.Console._addToolbarButton([
			{	text: 'Clear',
				handler: gm.Console.clear
			},
			{	text: 'OnOff',
				handler: gm.Console.onoff
			},
			{	text: 'Test',
				handler: gm.Console.logTest
			},
			{	text: 'Close',
				handler: gm.Console.close
			}			
		]);
		
		// make it movable
		gm.Utils.dragDropper.add(gm.Console._consoleWrapper);
		
	},
	
	onoff : function(){
		gm.Console._console.style.display = gm.Console._console.style.display=='none' ? 'block': 'none';
	},
	
	log : function(msg, clear){
		if(gm.Console._console){
			gm.Console._console.innerHTML = gm.Console._formatText(msg) + (clear? '': gm.Console._console.innerHTML);
		}
	},
	
	logTest : function(){
		gm.Console.log('logTest');
	},
	
	clear : function(){
		gm.Console._counter = 0;
		gm.Console._console.innerHTML = '';			
	},
	
	close : function(){
		gm.Console._consoleWrapper.style.display = 'none';
	}	
}

// Ajax
gm.Ajaxx = {
	load: function (url, callback, userData){	// callback: function(r){}; userData: {}
		function insertAjaxHtml(hash, html){
			var id = 'ajax_'+hash;
			var div = $(id);
			if (!div) {
				div = gm.Dom._e('div');
				div.id = id;
				div.style.display = 'none';
				gm.Dom.add(div);
			}
			
			div.innerHTML = html;	
			return div;
		}
		
		function getAjaxHtmlBody(r){	// r: ajax response object
			var s = new String(r.responseText);
			s = s.replace(/\n/g, ' ');
			s = s.replace(/^.*<body[^>]*>(.*)<\/body>.*$/gi, '$1');	// get data inside <body>...</body>
			return s;
		}
		
		function switchAjaxHint(hash, onoff)	{
			var div = $(gm.Consts.OBJ_ID_AJAX_HINT);
			if(!div){
				div = gm.Dom._e('div');
				div.id = gm.Consts.OBJ_ID_AJAX_HINT;
				div.style.display = 'none';
				gm.Dom.add(div);
			}
			
			if(onoff == gm.Consts.ENUM_ONOFF.ON){
				div.innerHTML += '<nobr>'+hash+'</nobr><br>';
				div.style.display = 'block';	
			}else{
				var nobrs = gm.Dom.$x("//div[@id='"+gm.Consts.OBJ_ID_AJAX_HINT+"']/nobr");
				nobrs.once(function(nobr){
					if(nobr.innerHTML.indexOf(hash)>=0){
						var br = nobr.nextSibling;
						br.parentNode.removeChild(br);
						nobr.parentNode.removeChild(nobr);
						return true;
					}else{
						return false;
					}
				});
				nobrs = gm.Dom.$x("//div[@id='"+gm.Consts.OBJ_ID_AJAX_HINT+"']/nobr");
				if(nobrs.length==0)	div.style.display = 'none';	
			}
		}
	
		// append hash code
		var hash = gm.Utils.hash();
		url += (url.indexOf('?')>0? '&':'?') + 'hash=' + hash;
		
		
			
		try{
			switchAjaxHint(url, gm.Consts.ENUM_ONOFF.ON);
			
			new Ajax.Request(
				url, 
				{	method: 'get',
					onComplete: function(r){
						gm.Console.log(hash);
						//try{							
						//	switchAjaxHint(hash, gm.Consts.ENUM_ONOFF.OFF);
						//	var div = insertAjaxHtml(getAjaxHtmlBody(r));
						//	try{
						//		callback(div, userData);								
						//	}finally{ gm.Dom.del(div);	}
						//}catch(e){
						//	gm.Console.log(e);
						//}
					}
				}
			);
			
		}catch(e){
			gm.Console.log('_ajax(): '+url, gm.Consts.ENUM_GM_LOG.ERR);
		}
	}	
	
};
// properties
gm.extend({
	version: '0.2',
	url: new String(window.location.href),
	url_params: gm.Utils.getUrlParams(gm.url)
});

// format error message
function _err(e){
	// get error line number and re-throw it 
	if(e.stack){
		var r = e.stack.match(/:[\d]+/gi);	
		if(r){
			e.lineNumber -= parseInt(r[r.length-1].replace(':',''));
		}
	}
	e.lineNumber = 'Line: ' + e.lineNumber;	
	e.message = '[' + e.name + '] ' + e.message;
	e.fileName = null;	
	throw(e);	
}





// DOM misc =====================================================================================================


function _domCreateImgButton(info){	//{id, url, icon, target, title, func}
	var A_CLASS_BUTTON = 'linkButton';
	var iconButton = gm.Dom._e('a');
	iconButton.className = A_CLASS_BUTTON;
	
	iconButton.innerHTML = '<img border=0 src="' + info.img +'">';	
	
	if(info.id) iconButton.id = info.id;
	if(info.url) iconButton.href = info.url;
	if(info.target) iconButton.target = info.target;
	if(info.hint) iconButton.title = info.hint;
	
	if(info.attrs){
		info.attrs.each(function(attr){
			iconButton.setAttribute(attr[0], attr[1]);
		});
	}

	if(info.events){
		info.events.each(function(e){
			iconButton.addEventListener(e[0], e[1], true);
		});
	}
	
	return iconButton;
}




// Ajax =====================================================================================================
// =====================================================================================================
// Unit test
// =====================================================================================================
var GMLibTest = {
	run: function(){
		gm.Console.open('GMLib Unit Test');
		
		// String
		var s = ' This is a string. ';
		gm.Console.log('s = "'+s+'"');
		gm.Console.log('s.triml() = "'+s.triml()+'"');
		gm.Console.log('s.trimr() = "'+s.trimr()+'"');
		gm.Console.log('s.trim() = "'+s.trim()+'"');
		
		// Array
		var a = [
			'This',
			'is',
			'a',
			'string'
		];
		gm.Console.log('a[] = ['+a+']');
				
		gm.Console.log('a.lastIndexOf("is") = '+a.lastIndexOf("is"));
		
		gm.Console.log('a.random() = ' + a.random());
		
		a.shuffle();
		gm.Console.log('a.shuffle() = [' + a + ']');
				
		a.insert(2, "not");
		gm.Console.log('a.insert(2, "not") = ['+ a +']');
		
		var b = a.walk(function(v){
			return '('+v+')';
		});
		gm.Console.log('b = a.walk(f) = [' + b + ']');
		
		b = a.slice();
		b.push('is');
		gm.Console.log('b[] = ['+b+']');
		
		var a = b.unique();
		gm.Console.log('a = b.unique() = [' + a + ']');
		
		a.each(function(v,i){
			gm.Console.log('a.each(f): a['+i+'] = ' + v);
		});

		a.once(function(v){
			gm.Console.log('a.once(f(lookfor_a)): ' + v + (v=='a'? '*': ''));
			return (v=='a');
		});
					
	}
};


//GMLibTest.run();

